from __future__ import division

from ..attributes import *
from ..attributes import _
from ..flatten import *

from .structure import *


class Grid(Element):
    spec   = Attribute()
    width  = Attribute(initial = 0, writable = True)
    height = Attribute(initial = 0, writable = True)
    layout = Attribute(initiable = False, writable = True)
    
    data           = ListAttribute()
    row_layouts    = ListAttribute()
    column_layouts = ListAttribute()
    
    def setup(self):
        self.layout = GridLayout()
        
        if self.spec is not None:
            for row in self.spec:
                self.add_row(row)
    
    @property
    def is_empty(self):
        return self.width ==0 or self.height == 0
    
    @property
    def children(self):
        return list(iter(self))
    
    def at(self, x, y):
        if x < 1 or y < 1 or x > self.width or y > self.height:
            return None
        
        return self._data[y - 1][x - 1]
    
    def set(self, x, y, data):
        self.ensure_width(x)
        self.ensure_height(y)
        
        self._data[y - 1][x - 1] = data
    
    def add_row(self, data):
        self.ensure_width(len(data))
        
        self._height += 1        
        self._row_layouts.append(RowLayout(self, self.height))
        self._data.append(self.resize(data, self.width))
    
    def add_column(self, data):
        self.ensure_height(len(data))
        
        self._width += 1
        self._column_layouts.append(ColumnLayout(self, self.width))
        
        for num, item in enumerate(self.resize(data, self.height)):
            self._data[num].append(data)
    
    def row(self, row):
        return self.rows(row)
    
    def rows(self, *rows):
        return Cells(self, lambda: [(x, y) for x in range(1, self.width + 1) for y in rows],
                     lambda: [self._row_layouts[y - 1] for y in rows])
    
    def column(self, column):
        return self.columns(column)
    
    def columns(self, *columns):
        return Cells(self, lambda: [(x, y) for x in columns for y in range(1, self.height + 1)],
                     lambda: [self._column_layouts[x - 1] for x in columns])
    
    def ensure_width(self, num):
        if num <= self.width:
            return
        
        for row in range(self.height):
            self._data[row].append((None,) * (num - self.width))
        
        for delta in range(num - self.width):
            self._column_layouts.append(ColumnLayout(self, self.width + delta + 1))

        self._width = num
    
    def ensure_height(self, num):
        if num <= self.height:
            return
        
        for delta in range(num - self.height):
            self._data.append((None,) * self.width)
            self._row_layouts.append(RowLayout(self, self.height + delta + 1))
        
        self._height = num
    
    def resize(self, iterable, num):
        have = tuple(iterable)
        
        return have + (None,) * (num - len(have))
    
    def layout_in(self, area):
        blank_width  = int(self.layout.margin_x * area.width)  * 2 + (self.width - 1) * int(self.layout.padding_x * area.width)
        blank_height = int(self.layout.margin_y * area.height) * 2 + (self.width - 1) * int(self.layout.padding_y * area.height)
        set_width    = sum(column.fixed_width(area.width)  for column in self.column_layouts if column.expand_x is None)
        set_height   = sum(row.fixed_height  (area.height) for row    in self.row_layouts    if row.expand_y    is None)
        expand_x     = sum(column.expand_x    for column in self.column_layouts if column.expand_x is not None)
        expand_y     = sum(row.expand_y       for row    in self.row_layouts    if row.expand_y    is not None)
        left_width   = max(area.width  - set_width  - blank_width,  0)
        left_height  = max(area.height - set_height - blank_height, 0)
        widths       = []
        heights      = []
        
        # Distribute expanding space
        for column in self.column_layouts:
            if column.expand_x is None or self.layout.expand_x is None:
                widths.append(column.fixed_width(area.width))
            else:
                widths.append(left_width * column.expand_x // expand_x)
        
        for row in self.row_layouts:
            if row.expand_y is None or self.layout.expand_y is None:
                heights.append(row.fixed_height(area.height))
            else:
                heights.append(left_height * row.expand_y // expand_y)
        
        # Remove overhanging space proportionally
        if sum(widths) == 0 or sum(heights) == 0:
            return
        
        x_factor = min(area.width  / sum(widths),  1)
        y_factor = min(area.height / sum(heights), 1)
        
        for num in range(len(widths)):
            widths[num] = int(widths[num] * x_factor)
        
        for num in range(len(heights)):
            heights[num] = int(heights[num] * y_factor)
        
        # Get rid of rounding errors
        if self.layout.expand_x is not None:
            for num, column in enumerate(self.column_layouts):
                if sum(widths) >= area.width:
                    break
                
                if column.expand_x is None:
                    continue
                
                widths[num] += min(column.expand_x, area.width - sum(widths))
        
        if self.layout.expand_y is not None:
            for num, row in enumerate(self.row_layouts):
                if sum(heights) >= area.height:
                    break
                
                if row.expand_y is None:
                    continue
                
                heights[num] += min(row.expand_y, area.height - sum(heights))
        
        # Assign space
        dx = int(self.layout.margin_x * area.width)
        dy = int(self.layout.margin_y * area.height)
        
        for x in range(self.width):
            for y in range(self.height):
                self.at(x + 1, y + 1).set_area(area.top_left.in_direction(dx, dy).extend_by(widths[x], heights[y]))
                
                dy += heights[y] + int(self.layout.padding_y * area.height)
            
            dx += widths[x] + int(self.layout.padding_x * area.width)
            dy  = int(self.layout.margin_y * area.height)
    
    @property
    def intrinsic_size(self):
        width  = sum(column.fixed_width(0) for column in self.column_layouts)
        height = sum(row.fixed_height(0)   for row    in self.row_layouts)
        
        # Calculate whitespace equilibrium
        mx = 2 * self.layout.margin_x
        my = 2 * self.layout.margin_y
        px = (self.width  - 1) * self.layout.padding_x
        py = (self.height - 1) * self.layout.padding_y
        bx = mx + px
        by = my + py
        
        width  *= 1 + bx / (1 - bx)
        height *= 1 + by / (1 - by)
        
        return (width, height)
    
    def __iter__(self):
        return iter(flattened(self.data))


class Cells(Object):
    grid    = Attribute()
    cells   = Attribute()
    layouts = Attribute()
    calls   = ListAttribute()
    
    def __getattr__(self, name):
        if name.startswith('_'):
            return super(Cells, self).__getattr__(name)
        else:
            return Cells(self.grid, self.cells, self.layouts, self.calls + [name])
    
    def __setattr__(self, name, value):
        if name.startswith('_') or name in ('grid', 'cells', 'calls', 'layouts'):
            super(Cells, self).__setattr__(name, value)
        elif self.calls == ['layout']:
            for layout in self.layouts():
                setattr(layout, name, value)
        else:
            for cell in self.cells():
                instance = self.grid.at(*cell)
                
                if instance is not None:
                    for call in self.calls:
                        instance = getattr(instance, call)
                    
                    setattr(instance, name, value)

    def __call__(self, *args, **kwds):
        for cell in self.cells():
            instance = self.grid.at(*cell)
            
            if instance is not None:
                for call in self.calls:
                    instance = getattr(instance, call)
                
                instance(*args, **kwds)


class GridLayout(Layout):
    margin_x  = Attribute(initial = 0, initiable = False, writable = True)
    margin_y  = Attribute(initial = 0, initiable = False, writable = True)
    padding_x = Attribute(initial = 0, initiable = False, writable = True)
    padding_y = Attribute(initial = 0, initiable = False, writable = True)


class ColumnLayout(Layout):
    grid = Attribute()
    num  = Attribute()
    
    height   =_ (None)
    expand_y =_ (None)
    
    def fixed_width(self, width):
        intrinsic = max(self.grid.at(*cell).intrinsic_size[0] for cell in self.grid.column(self.num).cells())
        
        if self.width is None:
            return intrinsic
        else:
            return max(int(self.width * width), intrinsic)


class RowLayout(Layout):
    grid = Attribute()
    num  = Attribute()
    
    width    =_ (None)
    expand_x =_ (None)
    
    def fixed_height(self, height):
        intrinsic = max(self.grid.at(*cell).intrinsic_size[1] for cell in self.grid.row(self.num).cells())
        
        if self.height is None:
            return intrinsic
        else:
            return max(int(self.height * height), intrinsic)
